package com.aelitis.azureus.core.neuronal;

public class NeuralNetworkLayer {

    int numberOfNodes;
    double[][] weights;
    double[][] weightChanges;
    double[] neuronValues;
    double[] desiredValues;
    double[] errors;
    double[] biasWeights;
    double[] biasValues;

    double learningRate;
    boolean linearOutput;
    boolean useMomentum;
    double momentumFactor;

    NeuralNetworkLayer parentLayer;
    NeuralNetworkLayer childLayer;

    ActivationFunction activationFunction;

    public NeuralNetworkLayer(int numberOfNodes) {

        this.numberOfNodes = numberOfNodes;

        linearOutput = false;
        useMomentum = false;
        momentumFactor = 0.9;
    }

    public void initialize(NeuralNetworkLayer parentLayer, NeuralNetworkLayer childLayer) {

        neuronValues = new double[numberOfNodes];
        desiredValues = new double[numberOfNodes];
        errors = new double[numberOfNodes];
        this.parentLayer = parentLayer;

        if (childLayer != null) {
            this.childLayer = childLayer;
            weights = new double[numberOfNodes][childLayer.getNumberOfNodes()];
            weightChanges = new double[numberOfNodes][childLayer.getNumberOfNodes()];
            biasValues = new double[childLayer.getNumberOfNodes()];
            biasWeights = new double[childLayer.getNumberOfNodes()];

            for (int j = 0; j < childLayer.getNumberOfNodes(); j++) {
                biasValues[j] = -1;
                biasWeights[j] = 0;
            }
        }
    }

    public void randomizeWeights() {
        for (int i = 0; i < numberOfNodes; i++) {
            for (int j = 0; j < childLayer.getNumberOfNodes(); j++) {
                weights[i][j] = Math.random() * 2 - 1;
            }
        }

        for (int j = 0; j < childLayer.getNumberOfNodes(); j++) {
            biasWeights[j] = Math.random() * 2 - 1;
        }
    }

    public void calculateNeuronValues() {
        if (parentLayer != null) {
            for (int j = 0; j < numberOfNodes; j++) {
                double x = 0;

                for (int i = 0; i < parentLayer.getNumberOfNodes(); i++) {
                    x += parentLayer.neuronValues[i] * parentLayer.weights[i][j];
                }

                x += parentLayer.biasValues[j] * parentLayer.biasWeights[j];

                if (childLayer == null && linearOutput) {
                    neuronValues[j] = x;
                } else {
                    neuronValues[j] = activationFunction.getValueFor(x);
                }
            }
        }
    }

    public void calculateErrors() {

        if (childLayer == null) {
            // output layer
            for (int i = 0; i < numberOfNodes; i++) {
                errors[i] = (desiredValues[i] - neuronValues[i]) * activationFunction.getDerivedFunctionValueFor(neuronValues[i]);
            }
        } else if (parentLayer == null) {
            // input layer
            for (int i = 0; i < numberOfNodes; i++) {
                errors[i] = 0.0;
            }
        } else {
            // hidden layer
            for (int i = 0; i < numberOfNodes; i++) {
                double sum = 0;
                for (int j = 0; j < childLayer.getNumberOfNodes(); j++) {
                    sum += childLayer.errors[j] * weights[i][j];
                }
                errors[i] = sum * activationFunction.getDerivedFunctionValueFor(neuronValues[i]);
            }
        }

    }

    public void adjustWeights() {
        if (childLayer != null) {
            for (int i = 0; i < numberOfNodes; i++) {
                for (int j = 0; j < childLayer.getNumberOfNodes(); j++) {
                    double dw = learningRate * childLayer.errors[j] * neuronValues[i];
                    if (useMomentum) {
                        weights[i][j] += dw + momentumFactor * weightChanges[i][j];
                        weightChanges[i][j] = dw;
                    } else {
                        weights[i][j] += dw;
                    }
                }
            }

            for (int j = 0; j < childLayer.getNumberOfNodes(); j++) {
                biasWeights[j] += learningRate * childLayer.errors[j] * biasValues[j];
            }
        }
    }

    public int getNumberOfNodes() {
        return numberOfNodes;
    }

    public void setActivationFunction(ActivationFunction activationFunction) {
        this.activationFunction = activationFunction;
    }

    public void setMomentum(boolean useMomentum, double factor) {
        this.useMomentum = useMomentum;
        this.momentumFactor = factor;
    }

    public void setLearningRate(double rate) {
        this.learningRate = rate;
    }

    public String toString() {
        StringBuffer sb = new StringBuffer();
        if (childLayer != null) {
            for (int j = 0; j < childLayer.getNumberOfNodes(); j++) {
                sb.append(j);
                sb.append("\t> ");
                for (int i = 0; i < numberOfNodes; i++) {
                    sb.append(i);
                    sb.append(":");
                    sb.append(weights[i][j]);
                    sb.append("\t");
                }
                sb.append("\n");
            }
        }

        return sb.toString();
    }

}
